TextView 源码解读篇

OverView

继承关系

TextView_OverView

onMeasure

调用流程

时序图

SequenceDiagram1

背景知识

关于源码

onMeasure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
  @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//get the width and height from parent(decide by parent's layoutParam and view's layoutParam )
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

int width;
int height;

BoringLayout.Metrics boring = UNKNOWN_BORING;
BoringLayout.Metrics hintBoring = UNKNOWN_BORING;

//get TextDirectionHeuristic(default:LTR)
if (mTextDir == null) {
mTextDir = getTextDirectionHeuristic();
}

int des = -1;
boolean fromexisting = false;

if (widthMode == MeasureSpec.EXACTLY) {
// Parent has told us how big to be. So be it.
width = widthSize;
} else {
if (mLayout != null && mEllipsize == null) {
//desired(if all of lineEnd is not '\n' , marks it as singleLine and return -1;or renture the max of line witdh)
des = desired(mLayout);
}

//singleLine
if (des < 0) {
//input texts paint direction fontmetrics,if it is singLine return the width of given tex and metrics
boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
if (boring != null) {
mBoring = boring;
}
} else {
fromexisting = true;
}

if (boring == null || boring == UNKNOWN_BORING) {
//not SingleLine
if (des < 0) {
//find the max width of lines,just from the source of texts
des = (int) Math.ceil(Layout.getDesiredWidth(mTransformed, mTextPaint));
}
width = des;
} else {
width = boring.width;
}
//-------from text---------above has the width wether from boringLayout or from getDesiredWidth----
//compare with the drawable width
final Drawables dr = mDrawables;
if (dr != null) {
//if drawableTop or drawableBottom is wider than texts
width = Math.max(width, dr.mDrawableWidthTop);
width = Math.max(width, dr.mDrawableWidthBottom);
}
//--------from drawable ---------above compare width with drawable ---------------
//hint width
if (mHint != null) {
int hintDes = -1;
int hintWidth;

if (mHintLayout != null && mEllipsize == null) {
hintDes = desired(mHintLayout);
}

if (hintDes < 0) {
hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir, mHintBoring);
if (hintBoring != null) {
mHintBoring = hintBoring;
}
}

if (hintBoring == null || hintBoring == UNKNOWN_BORING) {
if (hintDes < 0) {
hintDes = (int) Math.ceil(Layout.getDesiredWidth(mHint, mTextPaint));
}
hintWidth = hintDes;
} else {
hintWidth = hintBoring.width;
}

if (hintWidth > width) {
width = hintWidth;
}
}
//-------from hinttext-------------------above compare width with hinttext same process as text---
width += getCompoundPaddingLeft() + getCompoundPaddingRight();

//ems limit
if (mMaxWidthMode == EMS) {
//I don't know why to compare the width with getLineHeight()
width = Math.min(width, mMaxWidth * getLineHeight());
} else {
width = Math.min(width, mMaxWidth);
}

if (mMinWidthMode == EMS) {
width = Math.max(width, mMinWidth * getLineHeight());
} else {
width = Math.max(width, mMinWidth);
}
//----------from ems----------------above compare width with ems value -----------------
// Check against our minimum width
width = Math.max(width, getSuggestedMinimumWidth());
//------------from backgourd ---------above compare width with background width----
if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(widthSize, width);
}
// -----from parent width -------------above compare width with parent width
}

int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight();
int unpaddedWidth = want;

if (mHorizontallyScrolling) want = VERY_WIDE;

int hintWant = want;
int hintWidth = (mHintLayout == null) ? hintWant : mHintLayout.getWidth();

if (mLayout == null) {
// :here to find the right layout eg:staticLayout ,dynamicLayout or boringLayout
// process:makeNewLayout->makeSingleLayout
makeNewLayout(want, hintWant, boring, hintBoring,
width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
} else {
final boolean layoutChanged = (mLayout.getWidth() != want) ||
(hintWidth != hintWant) ||
(mLayout.getEllipsizedWidth() !=
width - getCompoundPaddingLeft() - getCompoundPaddingRight());

final boolean widthChanged = (mHint == null) &&
(mEllipsize == null) &&
(want > mLayout.getWidth()) &&
(mLayout instanceof BoringLayout || (fromexisting && des >= 0 && des <= want));

final boolean maximumChanged = (mMaxMode != mOldMaxMode) || (mMaximum != mOldMaximum);

if (layoutChanged || maximumChanged) {
//if layoutChanged or maxumunChanged
if (!maximumChanged && widthChanged) {
//if only widthChanged just incerease
mLayout.increaseWidthTo(want);
} else {
//recongnize it as new one
makeNewLayout(want, hintWant, boring, hintBoring,
width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
}
} else {
// Nothing has changed
}
}
//---------------------------------------------------height start ------------------
if (heightMode == MeasureSpec.EXACTLY) {
// Parent has told us how big to be. So be it.
height = heightSize;
mDesiredHeightAtMeasure = -1;
} else {
//condition:hintHeight,textHeight,MaxLine,MinLine.drawableLeft and drawableRight;
//the desired height contains the padHeight
int desired = getDesiredHeight();

height = desired;
mDesiredHeightAtMeasure = desired;

if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(desired, heightSize);
}
//---from parent height ------compare height with parent height------------
}

//unpaddedHeight wheter the totalHeight -padding or maxLineHeight(in MaxMode == Lines)
int unpaddedHeight = height - getCompoundPaddingTop() - getCompoundPaddingBottom();
if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) {
unpaddedHeight = Math.min(unpaddedHeight, mLayout.getLineTop(mMaximum));
}

/*
* We didn't let makeNewLayout() register to bring the cursor into view,
* so do it here if there is any possibility that it is needed.
*/
//by setMovementMethod textView could scroll,if space is not enough
if (mMovement != null ||
mLayout.getWidth() > unpaddedWidth ||
mLayout.getHeight() > unpaddedHeight) {
registerForPreDraw();
} else {
scrollTo(0, 0);
}

//set the final values
setMeasuredDimension(width, height);
}

makeNewLayout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
/**
* The width passed in is now the desired layout width,
* not the full view width with padding.
* {@hide}
*/
protected void makeNewLayout(int wantWidth, int hintWidth,
BoringLayout.Metrics boring,
BoringLayout.Metrics hintBoring,
int ellipsisWidth, boolean bringIntoView) {
stopMarquee();

// Update "old" cached values
mOldMaximum = mMaximum;
mOldMaxMode = mMaxMode;

mHighlightPathBogus = true;

if (wantWidth < 0) {
wantWidth = 0;
}
if (hintWidth < 0) {
hintWidth = 0;
}

Layout.Alignment alignment = getLayoutAlignment();
final boolean testDirChange = mSingleLine && mLayout != null &&
(alignment == Layout.Alignment.ALIGN_NORMAL ||
alignment == Layout.Alignment.ALIGN_OPPOSITE);
int oldDir = 0;
if (testDirChange) oldDir = mLayout.getParagraphDirection(0);
boolean shouldEllipsize = mEllipsize != null && getKeyListener() == null;
final boolean switchEllipsize = mEllipsize == TruncateAt.MARQUEE &&
mMarqueeFadeMode != MARQUEE_FADE_NORMAL;
TruncateAt effectiveEllipsize = mEllipsize;
if (mEllipsize == TruncateAt.MARQUEE &&
mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
effectiveEllipsize = TruncateAt.END_SMALL;
}

if (mTextDir == null) {
mTextDir = getTextDirectionHeuristic();
}

mLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment, shouldEllipsize,
effectiveEllipsize, effectiveEllipsize == mEllipsize);
if (switchEllipsize) {
TruncateAt oppositeEllipsize = effectiveEllipsize == TruncateAt.MARQUEE ?
TruncateAt.END : TruncateAt.MARQUEE;
mSavedMarqueeModeLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment,
shouldEllipsize, oppositeEllipsize, effectiveEllipsize != mEllipsize);
}

shouldEllipsize = mEllipsize != null;
mHintLayout = null;

if (mHint != null) {
if (shouldEllipsize) hintWidth = wantWidth;

if (hintBoring == UNKNOWN_BORING) {
hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir,
mHintBoring);
if (hintBoring != null) {
mHintBoring = hintBoring;
}
}

if (hintBoring != null) {
if (hintBoring.width <= hintWidth &&
(!shouldEllipsize || hintBoring.width <= ellipsisWidth)) {
if (mSavedHintLayout != null) {
mHintLayout = mSavedHintLayout.
replaceOrMake(mHint, mTextPaint,
hintWidth, alignment, mSpacingMult, mSpacingAdd,
hintBoring, mIncludePad);
} else {
mHintLayout = BoringLayout.make(mHint, mTextPaint,
hintWidth, alignment, mSpacingMult, mSpacingAdd,
hintBoring, mIncludePad);
}

mSavedHintLayout = (BoringLayout) mHintLayout;
} else if (shouldEllipsize && hintBoring.width <= hintWidth) {
if (mSavedHintLayout != null) {
mHintLayout = mSavedHintLayout.
replaceOrMake(mHint, mTextPaint,
hintWidth, alignment, mSpacingMult, mSpacingAdd,
hintBoring, mIncludePad, mEllipsize,
ellipsisWidth);
} else {
mHintLayout = BoringLayout.make(mHint, mTextPaint,
hintWidth, alignment, mSpacingMult, mSpacingAdd,
hintBoring, mIncludePad, mEllipsize,
ellipsisWidth);
}
}
}
// TODO: code duplication with makeSingleLayout()
if (mHintLayout == null) {
StaticLayout.Builder builder = StaticLayout.Builder.obtain(mHint, 0,
mHint.length(), mTextPaint, hintWidth)
.setAlignment(alignment)
.setTextDirection(mTextDir)
.setLineSpacing(mSpacingAdd, mSpacingMult)
.setIncludePad(mIncludePad)
.setBreakStrategy(mBreakStrategy)
.setHyphenationFrequency(mHyphenationFrequency);
if (shouldEllipsize) {
builder.setEllipsize(mEllipsize)
.setEllipsizedWidth(ellipsisWidth)
.setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
}
mHintLayout = builder.build();
}
}

if (bringIntoView || (testDirChange && oldDir != mLayout.getParagraphDirection(0))) {
registerForPreDraw();
}

if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
if (!compressText(ellipsisWidth)) {
final int height = mLayoutParams.height;
// If the size of the view does not depend on the size of the text, try to
// start the marquee immediately
if (height != LayoutParams.WRAP_CONTENT && height != LayoutParams.MATCH_PARENT) {
startMarquee();
} else {
// Defer the start of the marquee until we know our width (see setFrame())
mRestartMarquee = true;
}
}
}

// CursorControllers need a non-null mLayout
if (mEditor != null) mEditor.prepareCursorControllers();
}

makeSingleLayout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
private Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
Layout.Alignment alignment, boolean shouldEllipsize, TruncateAt effectiveEllipsize,
boolean useSaved) {
Layout result = null;
if (mText instanceof Spannable) {
result = new DynamicLayout(mText, mTransformed, mTextPaint, wantWidth,
alignment, mTextDir, mSpacingMult, mSpacingAdd, mIncludePad,
mBreakStrategy, mHyphenationFrequency,
getKeyListener() == null ? effectiveEllipsize : null, ellipsisWidth);
} else {
if (boring == UNKNOWN_BORING) {
boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
if (boring != null) {
mBoring = boring;
}
}

if (boring != null) {
if (boring.width <= wantWidth &&
(effectiveEllipsize == null || boring.width <= ellipsisWidth)) {
if (useSaved && mSavedLayout != null) {
result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
wantWidth, alignment, mSpacingMult, mSpacingAdd,
boring, mIncludePad);
} else {
result = BoringLayout.make(mTransformed, mTextPaint,
wantWidth, alignment, mSpacingMult, mSpacingAdd,
boring, mIncludePad);
}

if (useSaved) {
mSavedLayout = (BoringLayout) result;
}
} else if (shouldEllipsize && boring.width <= wantWidth) {
if (useSaved && mSavedLayout != null) {
result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
wantWidth, alignment, mSpacingMult, mSpacingAdd,
boring, mIncludePad, effectiveEllipsize,
ellipsisWidth);
} else {
result = BoringLayout.make(mTransformed, mTextPaint,
wantWidth, alignment, mSpacingMult, mSpacingAdd,
boring, mIncludePad, effectiveEllipsize,
ellipsisWidth);
}
}
}
}
if (result == null) {
StaticLayout.Builder builder = StaticLayout.Builder.obtain(mTransformed,
0, mTransformed.length(), mTextPaint, wantWidth)
.setAlignment(alignment)
.setTextDirection(mTextDir)
.setLineSpacing(mSpacingAdd, mSpacingMult)
.setIncludePad(mIncludePad)
.setBreakStrategy(mBreakStrategy)
.setHyphenationFrequency(mHyphenationFrequency);
if (shouldEllipsize) {
builder.setEllipsize(effectiveEllipsize)
.setEllipsizedWidth(ellipsisWidth)
.setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
}
// TODO: explore always setting maxLines
result = builder.build();
}
return result;
}

关键路径

TextView_Path

如何实现

行间距和字间距实现

  • com.android.internal.R.styleable.TextView_lineSpacingMultiplier:

    com.android.internal.R.styleable.TextView_lineSpacingExtra:

  • com.android.internal.R.styleable.TextView_letterSpacing: api_level 21 5.0

    TextPaint开放接口提供功能实现

如何判断换行

1
2
3
4
5
6
7
8
9
10
11
12
                 
static jint nComputeLineBreaks(JNIEnv* env, jclass, jlong nativePtr,
jobject recycle, jintArray recycleBreaks,
jfloatArray recycleWidths, jintArray recycleFlags,
jint recycleLength) {
LineBreaker* b = reinterpret_cast<LineBreaker*>(nativePtr);
size_t nBreaks = b->computeBreaks();
recycleCopy(env, recycle, recycleBreaks, recycleWidths, recycleFlags, recycleLength,
nBreaks, b->getBreaks(), b->getWidths(), b->getFlags());
b->finish();
return static_cast<jint>(nBreaks);
}

如何实现文字省略

setBreakStrategy api_level 23 android 6.0

values below: https://developer.android.com/reference/android/text/Layout.html#BREAK_STRATEGY_SIMPLE

如何实现emoji的统一替换

在settext之前把text替换成spannabelstring,并且插入imageSpan

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public void setText(CharSequence text, BufferType type) {
if((this.supportEmoji || this.supportEmotion) && text != null) {
SpannableStringBuilder builder;
if(this.supportEmoji) {
if(!(text instanceof SpannableStringBuilder) && !(text instanceof SpannableString)) {
text = EmojiUtil.ubb2utf(((CharSequence)text).toString());
}

builder = new SpannableStringBuilder((CharSequence)text);
this.replaceCount = APEmojiRender.renderEmojiReturncount(this.getContext(), builder, this.getEmojiSize());
} else {
builder = new SpannableStringBuilder((CharSequence)text);
}

if(this.supportEmotion && parseEmotionListener != null) {
this.hasEmotion = parseEmotionListener.parser(this.getContext(), builder, (CharSequence)text, this.getEmojiSize());
}

if((this.replaceCount > 0 || this.hasEmotion) && this.getEllipsize() != null) {
int singleLine = this.isSingleLine();
if(singleLine == 1) {
this.singeLineRender(builder, type);
} else {
super.setText(builder, type);
}
} else {
super.setText(builder, type);
}

if(this.onTextChangeListener != null && text != null) {
this.onTextChangeListener.onTextViewTextChange(this, builder.toString());
}
} else {
super.setText((CharSequence)text, type);
if(this.onTextChangeListener != null && text != null) {
this.onTextChangeListener.onTextViewTextChange(this, ((CharSequence)text).toString());
}
}

}

TextView_Emoji

onLayout

调用流程

背景知识

关于源码

onLayout

1
2
3
4
5
6
7
8
9
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (mDeferScroll >= 0) {
int curs = mDeferScroll;
mDeferScroll = -1;
bringPointIntoView(Math.min(curs, mText.length()));
}
}

如何实现

onLayout的事情主要还是交给 BoringLayout,DynamicLayout和StaticLayout

onDraw

调用流程

onDraw时序图

背景知识

View的draw做了些什么

1
2
3
4
5
6
7
8
9
10
11
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background//画背景色
* 2. If necessary, save the canvas' layers to prepare for fading//保存渐变图层
* 3. Draw view's content//画内容 onDraw,所有的view子类都是通过它提供不同的视觉效果的
* 4. Draw children//到viewgroup那里去绕一圈,在通过child.draw回来
* 5. If necessary, draw the fading edges and restore layers//画渐变色图层,在开发中实际用的不多
* 6. Draw decorations (scrollbars for instance)//画一些装饰门面,比如滚动条
*/

onDraw()

  • [ ]
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
        @Override
    protected void onDraw(Canvas canvas) {
    restartMarqueeIfNeeded();

    // Draw the background for this view
    super.onDraw(canvas);

    final int compoundPaddingLeft = getCompoundPaddingLeft();
    final int compoundPaddingTop = getCompoundPaddingTop();
    final int compoundPaddingRight = getCompoundPaddingRight();
    final int compoundPaddingBottom = getCompoundPaddingBottom();
    final int scrollX = mScrollX;
    final int scrollY = mScrollY;
    final int right = mRight;
    final int left = mLeft;
    final int bottom = mBottom;
    final int top = mTop;
    final boolean isLayoutRtl = isLayoutRtl();
    final int offset = getHorizontalOffsetForDrawables();
    final int leftOffset = isLayoutRtl ? 0 : offset;
    final int rightOffset = isLayoutRtl ? offset : 0 ;

    //----------step1 ---------drawableLeft....------------------------------
    final Drawables dr = mDrawables;
    if (dr != null) {
    /*
    * Compound, not extended, because the icon is not clipped
    * if the text height is smaller.
    */

    int vspace = bottom - top - compoundPaddingBottom - compoundPaddingTop;
    int hspace = right - left - compoundPaddingRight - compoundPaddingLeft;

    // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
    // Make sure to update invalidateDrawable() when changing this code.
    //理解下面的代码,可以知道drawableLeft或者其他,比如2行会怎么显示,比如有自带滚动条会怎么显示
    if (dr.mShowing[Drawables.LEFT] != null) {
    canvas.save();
    //最左边的中点开始画leftDrawable,paddingLef和scrollX在drawable的左边
    //翻阅资料 scrollX是滚动的偏移量(起点-终点坐标),+scrollX保证了图片不会随着跑马灯滚动
    canvas.translate(scrollX + mPaddingLeft + leftOffset,
    scrollY + compoundPaddingTop +
    (vspace - dr.mDrawableHeightLeft) / 2);
    dr.mShowing[Drawables.LEFT].draw(canvas);
    canvas.restore();
    }

    // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
    // Make sure to update invalidateDrawable() when changing this code.
    if (dr.mShowing[Drawables.RIGHT] != null) {
    canvas.save();
    //减的意思就是这个值在它的右边其作用,加代表这个值在他左边起作用
    canvas.translate(scrollX + right - left - mPaddingRight
    - dr.mDrawableSizeRight - rightOffset,
    scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightRight) / 2);
    dr.mShowing[Drawables.RIGHT].draw(canvas);
    canvas.restore();
    }

    // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
    // Make sure to update invalidateDrawable() when changing this code.
    if (dr.mShowing[Drawables.TOP] != null) {
    canvas.save();
    //最上面的中点开始画drawableTop,scrollX和paddingLeft始终在其左边
    canvas.translate(scrollX + compoundPaddingLeft +
    (hspace - dr.mDrawableWidthTop) / 2, scrollY + mPaddingTop);
    dr.mShowing[Drawables.TOP].draw(canvas);
    canvas.restore();
    }

    // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
    // Make sure to update invalidateDrawable() when changing this code.
    //x = scrollx+compaddingLeft+(hspace-drawableWidth)/2 -------bingo
    //y = scrollY+(bottom-top-compaddingButton-drawableHeight) -------bingo
    if (dr.mShowing[Drawables.BOTTOM] != null) {
    canvas.save();
    canvas.translate(scrollX + compoundPaddingLeft +
    (hspace - dr.mDrawableWidthBottom) / 2,
    scrollY + bottom - top - mPaddingBottom - dr.mDrawableSizeBottom);
    dr.mShowing[Drawables.BOTTOM].draw(canvas);
    canvas.restore();
    }
    }

    int color = mCurTextColor;

    if (mLayout == null) {
    assumeLayout();
    }

    Layout layout = mLayout;

    if (mHint != null && mText.length() == 0) {
    if (mHintTextColor != null) {
    color = mCurHintTextColor;
    }

    layout = mHintLayout;
    }

    mTextPaint.setColor(color);
    mTextPaint.drawableState = getDrawableState();
    //-------------step 2 ---------------文字阴影----------------------
    canvas.save();
    /* Would be faster if we didn't have to do this. Can we chop the
    (displayable) text so that we don't need to do this ever?
    */

    int extendedPaddingTop = getExtendedPaddingTop();
    int extendedPaddingBottom = getExtendedPaddingBottom();

    final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
    //TODO why getHeight() equals getLineTop(getLineCount)?//api注释应该在layout基类里实现了
    final int maxScrollY = mLayout.getHeight() - vspace;

    float clipLeft = compoundPaddingLeft + scrollX;
    float clipTop = (scrollY == 0) ? 0 : extendedPaddingTop + scrollY;
    float clipRight = right - left - getFudgedPaddingRight() + scrollX;
    float clipBottom = bottom - top + scrollY -
    ((scrollY == maxScrollY) ? 0 : extendedPaddingBottom);
    //TODO how to make mShadowRadius successfully
    //这里只是切范围,阴影应该在textPaint里面处理的
    if (mShadowRadius != 0) {
    clipLeft += Math.min(0, mShadowDx - mShadowRadius);
    clipRight += Math.max(0, mShadowDx + mShadowRadius);

    clipTop += Math.min(0, mShadowDy - mShadowRadius);
    clipBottom += Math.max(0, mShadowDy + mShadowRadius);
    }

    canvas.clipRect(clipLeft, clipTop, clipRight, clipBottom);

    int voffsetText = 0;
    int voffsetCursor = 0;

    // translate in by our padding
    /* shortcircuit calling getVerticaOffset() */
    if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
    voffsetText = getVerticalOffset(false);
    voffsetCursor = getVerticalOffset(true);
    }
    canvas.translate(compoundPaddingLeft, extendedPaddingTop + voffsetText);
    //---------step 3 ----------初始化跑马灯的初始化状态
    final int layoutDirection = getLayoutDirection();
    final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
    if (mEllipsize == TextUtils.TruncateAt.MARQUEE &&
    mMarqueeFadeMode != MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
    if (!mSingleLine && getLineCount() == 1 && canMarquee() &&
    (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) {
    final int width = mRight - mLeft;
    final int padding = getCompoundPaddingLeft() + getCompoundPaddingRight();
    final float dx = mLayout.getLineRight(0) - (width - padding);
    canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
    }

    if (mMarquee != null && mMarquee.isRunning()) {
    final float dx = -mMarquee.getScroll();
    canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
    }
    }

    final int cursorOffsetVertical = voffsetCursor - voffsetText;
    //-----------step 4 ------------------------画文字----------------------
    Path highlight = getUpdatedHighlightPath();
    if (mEditor != null) {
    mEditor.onDraw(canvas, layout, highlight, mHighlightPaint, cursorOffsetVertical);
    } else {
    layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
    }
    //-------------step 5 -----------------开始跑马灯的效果--------
    if (mMarquee != null && mMarquee.shouldDrawGhost()) {
    final float dx = mMarquee.getGhostOffset();
    canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
    layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
    }

    canvas.restore();
    }

Layout:draw

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Draw this Layout on the specified canvas, with the highlight path drawn
* between the background and the text.
*
* @param canvas the canvas
* @param highlight the path of the highlight or cursor; can be null
* @param highlightPaint the paint for the highlight
* @param cursorOffsetVertical the amount to temporarily translate the
* canvas while rendering the highlight
*/
public void draw(Canvas canvas, Path highlight, Paint highlightPaint,
int cursorOffsetVertical) {
//根据getClipBounds获取了top和bottom,然后组成了一个long值
final long lineRange = getLineRangeForDraw(canvas);
int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
if (lastLine < 0) return;

//highLight或者是linebackgroudSpan
drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical,
firstLine, lastLine);

drawText(canvas, firstLine, lastLine);
}

Layout:drawText

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
/**
* @hide
*/
public void drawText(Canvas canvas, int firstLine, int lastLine) {
int previousLineBottom = getLineTop(firstLine);
int previousLineEnd = getLineStart(firstLine);
ParagraphStyle[] spans = NO_PARA_SPANS;
int spanEnd = 0;
TextPaint paint = mPaint;
CharSequence buf = mText;

Alignment paraAlign = mAlignment;
TabStops tabStops = null;
boolean tabStopsIsInitialized = false;

TextLine tl = TextLine.obtain();

// Draw the lines, one at a time.
// The baseline is the top of the following line minus the current line's descent.
for (int lineNum = firstLine; lineNum <= lastLine; lineNum++) {
int start = previousLineEnd;
previousLineEnd = getLineStart(lineNum + 1);
int end = getLineVisibleEnd(lineNum, start, previousLineEnd);

int ltop = previousLineBottom;
int lbottom = getLineTop(lineNum + 1);
previousLineBottom = lbottom;
int lbaseline = lbottom - getLineDescent(lineNum);

int dir = getParagraphDirection(lineNum);
int left = 0;
int right = mWidth;

if (mSpannedText) {
Spanned sp = (Spanned) buf;
int textLength = buf.length();
boolean isFirstParaLine = (start == 0 || buf.charAt(start - 1) == '\n');

// New batch of paragraph styles, collect into spans array.
// Compute the alignment, last alignment style wins.
// Reset tabStops, we'll rebuild if we encounter a line with
// tabs.
// We expect paragraph spans to be relatively infrequent, use
// spanEnd so that we can check less frequently. Since
// paragraph styles ought to apply to entire paragraphs, we can
// just collect the ones present at the start of the paragraph.
// If spanEnd is before the end of the paragraph, that's not
// our problem.
if (start >= spanEnd && (lineNum == firstLine || isFirstParaLine)) {
spanEnd = sp.nextSpanTransition(start, textLength,
ParagraphStyle.class);
spans = getParagraphSpans(sp, start, spanEnd, ParagraphStyle.class);

paraAlign = mAlignment;
for (int n = spans.length - 1; n >= 0; n--) {
if (spans[n] instanceof AlignmentSpan) {
paraAlign = ((AlignmentSpan) spans[n]).getAlignment();
break;
}
}

tabStopsIsInitialized = false;
}

// Draw all leading margin spans. Adjust left or right according
// to the paragraph direction of the line.
final int length = spans.length;
boolean useFirstLineMargin = isFirstParaLine;
for (int n = 0; n < length; n++) {
if (spans[n] instanceof LeadingMarginSpan2) {
int count = ((LeadingMarginSpan2) spans[n]).getLeadingMarginLineCount();
int startLine = getLineForOffset(sp.getSpanStart(spans[n]));
// if there is more than one LeadingMarginSpan2, use
// the count that is greatest
if (lineNum < startLine + count) {
useFirstLineMargin = true;
break;
}
}
}
for (int n = 0; n < length; n++) {
if (spans[n] instanceof LeadingMarginSpan) {
LeadingMarginSpan margin = (LeadingMarginSpan) spans[n];
if (dir == DIR_RIGHT_TO_LEFT) {
margin.drawLeadingMargin(canvas, paint, right, dir, ltop,
lbaseline, lbottom, buf,
start, end, isFirstParaLine, this);
right -= margin.getLeadingMargin(useFirstLineMargin);
} else {
margin.drawLeadingMargin(canvas, paint, left, dir, ltop,
lbaseline, lbottom, buf,
start, end, isFirstParaLine, this);
left += margin.getLeadingMargin(useFirstLineMargin);
}
}
}
}

boolean hasTabOrEmoji = getLineContainsTab(lineNum);
// Can't tell if we have tabs for sure, currently
if (hasTabOrEmoji && !tabStopsIsInitialized) {
if (tabStops == null) {
tabStops = new TabStops(TAB_INCREMENT, spans);
} else {
tabStops.reset(TAB_INCREMENT, spans);
}
tabStopsIsInitialized = true;
}

// Determine whether the line aligns to normal, opposite, or center.
Alignment align = paraAlign;
if (align == Alignment.ALIGN_LEFT) {
align = (dir == DIR_LEFT_TO_RIGHT) ?
Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE;
} else if (align == Alignment.ALIGN_RIGHT) {
align = (dir == DIR_LEFT_TO_RIGHT) ?
Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL;
}

int x;
if (align == Alignment.ALIGN_NORMAL) {
if (dir == DIR_LEFT_TO_RIGHT) {
x = left + getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
} else {
x = right + getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
}
} else {
int max = (int)getLineExtent(lineNum, tabStops, false);
if (align == Alignment.ALIGN_OPPOSITE) {
if (dir == DIR_LEFT_TO_RIGHT) {
x = right - max + getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
} else {
x = left - max + getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
}
} else { // Alignment.ALIGN_CENTER
max = max & ~1;
x = ((right + left - max) >> 1) +
getIndentAdjust(lineNum, Alignment.ALIGN_CENTER);
}
}

paint.setHyphenEdit(getHyphen(lineNum));
Directions directions = getLineDirections(lineNum);
if (directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTabOrEmoji) {
//普通的
// XXX: assumes there's nothing additional to be done
canvas.drawText(buf, start, end, x, lbaseline, paint);
} else {
//emoji或者span的
tl.set(paint, buf, start, end, dir, directions, hasTabOrEmoji, tabStops);
tl.draw(canvas, x, ltop, lbaseline, lbottom);
}
paint.setHyphenEdit(0);
}

TextLine.recycle(tl);
}

Layout:elipsize+StaticLayout: getEllipsisCount+StaticLayout:getEllipsisStart

省略是怎么配合发生的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
        private void ellipsize(int start, int end, int line,
char[] dest, int destoff, TextUtils.TruncateAt method) {
int ellipsisCount = getEllipsisCount(line);

if (ellipsisCount == 0) {
return;
}

int ellipsisStart = getEllipsisStart(line);
int linestart = getLineStart(line);

for (int i = ellipsisStart; i < ellipsisStart + ellipsisCount; i++) {
char c;

if (i == ellipsisStart) {
c = getEllipsisChar(method); // ellipsis
} else {
c = '\uFEFF'; // 0-width space
}

int a = i + linestart;

if (a >= start && a < end) {
dest[destoff + a - start] = c;
}
}
}

---------------------------
/**
* Return the offset of the first character to be ellipsized away,
* relative to the start of the line. (So 0 if the beginning of the
* line is ellipsized, not getLineStart().)
*/
public abstract int getEllipsisStart(int line);
------------------------
/**
* Returns the number of characters to be ellipsized away, or 0 if
* no ellipsis is to take place.
*/
public abstract int getEllipsisCount(int line);

TextLine:draw

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/**
* Renders the TextLine.
*
* @param c the canvas to render on
* @param x the leading margin position
* @param top the top of the line
* @param y the baseline
* @param bottom the bottom of the line
*/
void draw(Canvas c, float x, int top, int y, int bottom) {
if (!mHasTabs) {
if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) {
drawRun(c, 0, mLen, false, x, top, y, bottom, false);
return;
}
if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) {
drawRun(c, 0, mLen, true, x, top, y, bottom, false);
return;
}
}

float h = 0;
int[] runs = mDirections.mDirections;
RectF emojiRect = null;

int lastRunIndex = runs.length - 2;
for (int i = 0; i < runs.length; i += 2) {
int runStart = runs[i];
int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK);
if (runLimit > mLen) {
runLimit = mLen;
}
boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0;

int segstart = runStart;
for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
int codept = 0;
Bitmap bm = null;

if (mHasTabs && j < runLimit) {
//根据emojicode找到对应的bitmap,然后画上去
codept = mChars[j];
if (codept >= 0xd800 && codept < 0xdc00 && j + 1 < runLimit) {
codept = Character.codePointAt(mChars, j);
if (codept >= Layout.MIN_EMOJI && codept <= Layout.MAX_EMOJI) {
bm = Layout.EMOJI_FACTORY.getBitmapFromAndroidPua(codept);
} else if (codept > 0xffff) {
++j;
continue;
}
}
}

if (j == runLimit || codept == '\t' || bm != null) {
h += drawRun(c, segstart, j, runIsRtl, x+h, top, y, bottom,
i != lastRunIndex || j != mLen);

if (codept == '\t') {
h = mDir * nextTab(h * mDir);
} else if (bm != null) {
float bmAscent = ascent(j);
float bitmapHeight = bm.getHeight();
float scale = -bmAscent / bitmapHeight;
float width = bm.getWidth() * scale;

if (emojiRect == null) {
emojiRect = new RectF();
}
emojiRect.set(x + h, y + bmAscent,
x + h + width, y);
c.drawBitmap(bm, null, emojiRect, mPaint);
h += width;
j++;
}
segstart = j + 1;
}
}
}
}

如何实现

跑马灯

效果实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Override
protected void onDraw(Canvas canvas) {
// If the size of the view does not depend on the size of the text, try to
// start the marquee immediately
// else Defer the start of the marquee until we know our width (see setFrame())
restartMarqueeIfNeeded();

// Draw the background for this view
super.onDraw(canvas);

''''''''''''''''''''''''''''''''''''''

final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
if (mEllipsize == TextUtils.TruncateAt.MARQUEE &&
mMarqueeFadeMode != MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
if (!mSingleLine && getLineCount() == 1 && canMarquee() &&
(absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) {
final int width = mRight - mLeft;
final int padding = getCompoundPaddingLeft() + getCompoundPaddingRight();
//getLineRight(0)
final float dx = mLayout.getLineRight(0) - (width - padding);
canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
}

if (mMarquee != null && mMarquee.isRunning()) {
//跑马灯位置更新的点
final float dx = -mMarquee.getScroll();
//跑马灯
canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
}
}

''''''''''''''''''''''
}
数值更新
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
private static final class Marquee {
''''''''''''''''''''''''''''''''''''''''''''''''''

private Choreographer.FrameCallback mTickCallback = new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
tick();
}
};

''''''''''''''''''''''''''''''''''''''''''''''
//滴答
void tick() {
if (mStatus != MARQUEE_RUNNING) {
return;
}

mChoreographer.removeFrameCallback(mTickCallback);

final TextView textView = mView.get();
if (textView != null && (textView.isFocused() || textView.isSelected())) {
long currentMs = mChoreographer.getFrameTime();
long deltaMs = currentMs - mLastAnimationMs;
mLastAnimationMs = currentMs;
float deltaPx = deltaMs / 1000f * mPixelsPerSecond;
//根据deltaMs计算出deltaPx的值,然后更新mScroll的值
mScroll += deltaPx;
if (mScroll > mMaxScroll) {
mScroll = mMaxScroll;
mChoreographer.postFrameCallbackDelayed(mRestartCallback, MARQUEE_DELAY);
} else {
mChoreographer.postFrameCallback(mTickCallback);
}
textView.invalidate();
}
}

''''''''''''''''''''''''''''''''''''''''
}

其他

文字缓存

https://github.com/facebookincubator/TextLayoutBuilder[Todo 0812]

Layout的方法论

TextView_Method

TODO

缺少两张图 (onMeasure 时序图, onDraw 时序图)